﻿using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Volo.Abp.Threading;

namespace Xms.Abp.Caching;

public class XmsMemoryCache : MemoryDistributedCache, IXmsCache
{
    static XmsMemoryCache()
    {
        var type = typeof(XmsMemoryCache);
        MemoryCacheField = type.GetField("_memCache", BindingFlags.Instance | BindingFlags.NonPublic);
        var cacheType = typeof(MemoryCache);
        EntriesField = cacheType.GetField("_entries", BindingFlags.Instance | BindingFlags.NonPublic);
        var entryType = cacheType.Assembly.GetType("Microsoft.Extensions.Caching.Memory.CacheEntry");
        var dictType = typeof(ConcurrentDictionary<,>).MakeGenericType(typeof(object), entryType);
        TryGetValueMethod = dictType.GetMethod("TryGetValue", new Type[]
        {
            typeof(object),
            Type.GetType(entryType.FullName + "&")
        });
        GetCacheEntryFunction = (key, cache) =>
        {
            var args = new object[] { key, null };
            TryGetValueMethod.Invoke(EntriesField.GetValue(cache), args);
            return args[1] as ICacheEntry;
        };
    }

    public XmsMemoryCache(IOptions<MemoryDistributedCacheOptions> optionsAccessor, ILoggerFactory loggerFactory)
        : base(optionsAccessor, loggerFactory)
    {
    }

    public string DataKeyName => throw new NotImplementedException();

    private static readonly FieldInfo MemoryCacheField;
    private static readonly FieldInfo EntriesField;
    private static readonly MethodInfo TryGetValueMethod;
    private static readonly Func<string, IMemoryCache, ICacheEntry> GetCacheEntryFunction;
    private IMemoryCache _MemoryCache;
    protected IMemoryCache MemoryCache
    {
        get
        {
            if (_MemoryCache == null)
            {
                _MemoryCache = MemoryCacheField.GetValue(this) as IMemoryCache;
            }
            return _MemoryCache;
        }
    }
    protected ConcurrentDictionary<object, ICacheEntry> EntriesCollection { get; }
    protected ICacheEntry GetCacheEntry(string key)
    {
        return GetCacheEntryFunction(key, MemoryCache);
    }

    public Task<DistributedCacheEntryOptions> GetExpirationFromCache(string key, CancellationToken cancellationToken = default)
    {
        var cacheEntry = GetCacheEntry(key);
        return Task.FromResult(new DistributedCacheEntryOptions
        {
            AbsoluteExpiration = cacheEntry.AbsoluteExpiration,
            AbsoluteExpirationRelativeToNow = cacheEntry.AbsoluteExpirationRelativeToNow,
            SlidingExpiration = cacheEntry.SlidingExpiration,
        });
    }

    public byte[][] GetMany(IEnumerable<string> keys)
    {
        return keys.Select(r => Get(r)).ToArray();
    }

    public async Task<byte[][]> GetManyAsync(IEnumerable<string> keys, CancellationToken cancellationToken = default)
    {
        var array = new byte[keys.Count()][];
        for (int i = 0; i < array.Length; i++)
        {
            array[i] = await GetAsync(keys.ElementAt(i));
        }
        return array;
    }

    public void RefreshMany(IEnumerable<string> keys)
    {
        foreach (var key in keys)
        {
            Refresh(key);
        }
    }

    public async Task RefreshManyAsync(IEnumerable<string> keys, CancellationToken cancellationToken = default)
    {
        foreach (var key in keys)
        {
            await RefreshAsync(key, cancellationToken);
        }
    }

    public void RemoveMany(IEnumerable<string> keys)
    {
        foreach (var key in keys)
        {
            Remove(key);
        }
    }

    public async Task RemoveManyAsync(IEnumerable<string> keys, CancellationToken cancellationToken = default)
    {
        foreach (var key in keys)
        {
            await RemoveAsync(key, cancellationToken);
        }
    }

    public void SetMany(IEnumerable<KeyValuePair<string, byte[]>> items, DistributedCacheEntryOptions options)
    {
        foreach (var item in items)
        {
            Set(item.Key, item.Value, options);
        }
    }

    public async Task SetManyAsync(IEnumerable<KeyValuePair<string, byte[]>> items, DistributedCacheEntryOptions options, CancellationToken cancellationToken = default)
    {
        foreach (var item in items)
        {
            await SetAsync(item.Key, item.Value, options, cancellationToken);
        }
    }

    public async Task<bool> KeyExistsAsync(string key, CancellationToken cancellationToken = default)
    {
        var value = await base.GetAsync(key, cancellationToken);
        return value != null;
    }

    private static readonly ConcurrentDictionary<string, object> KeyLockers = new();
    private static object GetKeyLocker(string key)
    {
        return KeyLockers.GetOrAdd(key, new object());
    }
    private static byte[] Int32ToBytes(int value)
    {
        byte[] src = new byte[4];
        src[3] = (byte)((value >> 24) & 0xFF);
        src[2] = (byte)((value >> 16) & 0xFF);
        src[1] = (byte)((value >> 8) & 0xFF);
        src[0] = (byte)(value & 0xFF);
        return src;
    }
    private static int BytesToInt32(byte[] src)
    {
        int value;
        value = (int)((src[0] & 0xFF)
                | ((src[1] & 0xFF) << 8)
                | ((src[2] & 0xFF) << 16)
                | ((src[3] & 0xFF) << 24));
        return value;
    }
    public async Task<long> GetSerialNumberAsync(string key, int quantity = 1, TimeSpan? expiration = null, CancellationToken cancellationToken = default)
    {
        await Task.CompletedTask;
        var value = 0;
        lock (GetKeyLocker(key))
        {
            var bytes = AsyncHelper.RunSync(async () => await GetAsync(key, cancellationToken));
            DistributedCacheEntryOptions options;
            if (bytes != null)
            {
                value = BytesToInt32(bytes);
                bytes = Int32ToBytes(value + quantity);
                options = AsyncHelper.RunSync(async () => await GetExpirationFromCache(key, cancellationToken));
            }
            else
            {
                bytes = Int32ToBytes(quantity);
                options = expiration?.TotalMilliseconds > 0 ? new DistributedCacheEntryOptions
                {
                    AbsoluteExpirationRelativeToNow = expiration
                } : new DistributedCacheEntryOptions();
            }
            AsyncHelper.RunSync(async () => await SetAsync(key, bytes, options, cancellationToken));
            value++;
        }
        return value;
    }

    private class MemoryLockEntry
    {
        public int InitialCount { get; set; }
        public int Value { get; set; }
    }

    public async Task<bool> LockExtendAsync(string key, TimeSpan? expiration = null, CancellationToken cancellationToken = default)
    {
        await Task.CompletedTask;
        var value = false;
        lock (GetKeyLocker(key))
        {
            if (MemoryCache.TryGetValue(key, out MemoryLockEntry entry))
            {
                if (expiration.HasValue)
                {
                    MemoryCache.Set(key, entry, expiration.Value);
                }
                else
                {
                    MemoryCache.Set(key, entry);
                }
            }
        }
        return value;
    }

    public async Task<bool> LockReleaseAsync(string key, int releaseCount = 1, CancellationToken cancellationToken = default)
    {
        await Task.CompletedTask;
        var value = false;
        lock (GetKeyLocker(key))
        {
            if (MemoryCache.TryGetValue(key, out MemoryLockEntry entry))
            {
                if (entry.Value > releaseCount)
                {
                    entry.Value -= releaseCount;
                    value = true;
                }
                else if (entry.Value == releaseCount)
                {
                    MemoryCache.Remove(key);
                    value = true;
                }
            }
        }
        return value;
    }

    public async Task<bool> LockTaskAsync(string key, int initialCount = 1, TimeSpan? expiration = null, CancellationToken cancellationToken = default)
    {
        await Task.CompletedTask;
        var value = false;
        lock (GetKeyLocker(key))
        {
            if (MemoryCache.TryGetValue(key, out MemoryLockEntry entry))
            {
                if (entry.Value < entry.InitialCount)
                {
                    entry.Value++;
                    value = true;
                }
            }
            else
            {
                entry = new MemoryLockEntry
                {
                    InitialCount = initialCount,
                    Value = 1
                };
                if (expiration.HasValue)
                {
                    MemoryCache.Set(key, entry, expiration.Value);
                }
                else
                {
                    MemoryCache.Set(key, entry);
                }
                value = true;
            }
        }
        return value;
    }

    public async Task<IEnumerable<KeyValuePair<string, byte[]>>> GetHashAllAsync(string key, CancellationToken cancellationToken = default)
    {
        await Task.CompletedTask;
        if (MemoryCache.TryGetValue(key, out ConcurrentDictionary<string, byte[]> hashs))
        {
            return hashs;
        }
        return null;
    }

    public async Task<byte[]> GetHashAsync(string key, string field, CancellationToken cancellationToken = default)
    {
        await Task.CompletedTask;
        if (MemoryCache.TryGetValue(key, out ConcurrentDictionary<string, byte[]> hashs))
        {
            if (hashs.ContainsKey(field))
            {
                return hashs[field];
            }
        }
        return null;
    }

    public async Task<IEnumerable<string>> GetHashKeysAsync(string key, CancellationToken cancellationToken = default)
    {
        await Task.CompletedTask;
        if (MemoryCache.TryGetValue(key, out ConcurrentDictionary<string, byte[]> hashs))
        {
            return hashs.Keys;
        }
        return null;
    }

    public async Task<IEnumerable<KeyValuePair<string, byte[]>>> GetHashManyAsync(string key, IEnumerable<string> fields, CancellationToken cancellationToken = default)
    {
        await Task.CompletedTask;
        if (MemoryCache.TryGetValue(key, out ConcurrentDictionary<string, byte[]> hashs))
        {
            return fields.Select(r => new KeyValuePair<string, byte[]>(r, hashs.ContainsKey(r) ? hashs[r] : null));
        }
        return null;
    }

    public async Task<bool> HashFieldExsitsAsync(string key, string field, CancellationToken cancellationToken = default)
    {
        await Task.CompletedTask;
        if (MemoryCache.TryGetValue(key, out ConcurrentDictionary<string, byte[]> hashs))
        {
            return hashs.ContainsKey(field);
        }
        return false;
    }

    public async Task RemoveHashAsync(string key, string field, CancellationToken cancellationToken = default)
    {
        await Task.CompletedTask;
        if (MemoryCache.TryGetValue(key, out ConcurrentDictionary<string, byte[]> hashs))
        {
            hashs.TryRemove(field, out _);
        }
    }

    public async Task SetHashAsync(string key, string field, byte[] value, CancellationToken cancellationToken = default)
    {
        await Task.CompletedTask;
        if (MemoryCache.TryGetValue(key, out ConcurrentDictionary<string, byte[]> hashs))
        {
            hashs.AddOrUpdate(field, value, (f, v) => value);
        }
    }

    public async Task SetHashManyAsync(string key, IEnumerable<KeyValuePair<string, byte[]>> items, CancellationToken cancellationToken = default)
    {
        await Task.CompletedTask;
        if (MemoryCache.TryGetValue(key, out ConcurrentDictionary<string, byte[]> hashs))
        {
            foreach (var item in items)
            {
                hashs.AddOrUpdate(item.Key, item.Value, (f, v) => item.Value);
            }
        }
    }


    const double PI = 3.14159265358979324;
    private double ComputeDistance(GeoPositionItem position1, GeoPositionItem position2)
    {
        var earthR = 6371000;
        var x = Math.Cos(position1.Latitude * PI / 180) * Math.Cos(position2.Latitude * PI / 180) * Math.Cos((position1.Longitude - position2.Longitude) * PI / 180);
        var y = Math.Sin(position1.Latitude * PI / 180) * Math.Sin(position2.Latitude * PI / 180);
        var s = x + y;
        if (s > 1) s = 1;
        if (s < -1) s = -1;
        var alpha = Math.Acos(s);
        var distance = alpha * earthR;
        return distance;
    }
    public async Task<double?> GeoDistanceAsync(string key, string member1, string member2, CancellationToken cancellationToken = default)
    {
        await Task.CompletedTask;
        if (MemoryCache.TryGetValue(key, out ConcurrentDictionary<string, GeoPositionItem> geos))
        {
            if (geos.ContainsKey(member1) && geos.ContainsKey(member2))
            {
                return ComputeDistance(geos[member1], geos[member2]);
            }
        }
        return null;
    }

    public async Task<GeoRadiusItem[]> GeoRadiusAsync(string key, string member, double radius, CancellationToken cancellationToken = default)
    {
        await Task.CompletedTask;
        if (MemoryCache.TryGetValue(key, out ConcurrentDictionary<string, GeoPositionItem> geos))
        {
            if (geos.ContainsKey(member))
            {
                var list = new List<GeoRadiusItem>();
                var srcPosition = geos[member];
                foreach (var mbr in geos.Keys)
                {
                    if (mbr != member)
                    {
                        var distPosition = geos[mbr];
                        var distance = ComputeDistance(srcPosition, distPosition);
                        if (distance < radius)
                        {
                            list.Add(new GeoRadiusItem
                            {
                                Distance = distance,
                                Member = mbr,
                                Position = distPosition
                            });
                        }
                    }
                }
                return list.ToArray();
            }
        }

        return null;
    }

    public async Task<GeoRadiusItem[]> GeoRadiusAsync(string key, double longitude, double latitude, double radius, CancellationToken cancellationToken = default)
    {
        await Task.CompletedTask;
        if (MemoryCache.TryGetValue(key, out ConcurrentDictionary<string, GeoPositionItem> geos))
        {
            var list = new List<GeoRadiusItem>();
            var srcPosition = new GeoPositionItem { Latitude = latitude, Longitude = longitude };
            foreach (var mbr in geos.Keys)
            {
                var distPosition = geos[mbr];
                var distance = ComputeDistance(srcPosition, distPosition);
                if (distance < radius)
                {
                    list.Add(new GeoRadiusItem
                    {
                        Distance = distance,
                        Member = mbr,
                        Position = distPosition
                    });
                }
            }
            return list.ToArray();
        }

        return null;
    }

    public async Task<GeoPositionItem> GetGeoAsync(string key, string member, CancellationToken cancellationToken = default)
    {
        await Task.CompletedTask;
        if (MemoryCache.TryGetValue(key, out ConcurrentDictionary<string, GeoPositionItem> geos))
        {
            if (geos.ContainsKey(member))
            {
                return geos[member];
            }
        }

        return null;
    }

    public async Task<IEnumerable<KeyValuePair<string, GeoPositionItem>>> GetGeoManyAsync(string key, string[] members, CancellationToken cancellationToken = default)
    {
        await Task.CompletedTask;
        if (MemoryCache.TryGetValue(key, out ConcurrentDictionary<string, GeoPositionItem> geos))
        {
            return members.ToDictionary(r => r, r => geos.ContainsKey(r) ? geos[r] : null);
        }

        return null;
    }

    public async Task<IEnumerable<KeyValuePair<string, GeoPositionItem>>> GetGeoManyAsync(string key, IEnumerable<string> members, CancellationToken cancellationToken = default)
    {
        await Task.CompletedTask;
        if (MemoryCache.TryGetValue(key, out ConcurrentDictionary<string, GeoPositionItem> geos))
        {
            return members.ToDictionary(r => r, r => geos.ContainsKey(r) ? geos[r] : null);
        }

        return null;
    }

    public async Task<bool> RemoveGeoAsync(string key, string member, CancellationToken cancellationToken = default)
    {
        await Task.CompletedTask;
        if (MemoryCache.TryGetValue(key, out ConcurrentDictionary<string, GeoPositionItem> geos))
        {
            return geos.TryRemove(member, out _);
        }

        return false;
    }

    public async Task SetGeoAsync(string key, string member, double longitude, double latitude, CancellationToken cancellationToken = default)
    {
        await Task.CompletedTask;
        if (MemoryCache.TryGetValue(key, out ConcurrentDictionary<string, GeoPositionItem> geos))
        {
            var position = new GeoPositionItem { Latitude = latitude, Longitude = longitude };
            geos.AddOrUpdate(member, position, (r, v) => position);
        }
    }

    public async Task SetGeoAsync(string key, string member, GeoPositionItem position, CancellationToken cancellationToken = default)
    {
        await Task.CompletedTask;
        if (MemoryCache.TryGetValue(key, out ConcurrentDictionary<string, GeoPositionItem> geos))
        {
            geos.AddOrUpdate(member, position, (r, v) => position);
        }
    }

    public async Task SetGeoManyAsync(string key, IEnumerable<KeyValuePair<string, GeoPositionItem>> geos, CancellationToken cancellationToken = default)
    {
        await Task.CompletedTask;
        if (MemoryCache.TryGetValue(key, out ConcurrentDictionary<string, GeoPositionItem> dict))
        {
            foreach(var item in geos)
            {
                dict.AddOrUpdate(item.Key, item.Value, (r, v) => item.Value);
            }
        }
    }
}
